package game;

import game.console.GameConsole;
import game.gfx.AbstractRunnable;
import game.gfx.Fullscreen;
import game.gfx.IRenderer;
import game.util.Logger;

import java.awt.BorderLayout;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.image.BufferedImage;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.SwingUtilities;

public abstract class AbstractGame extends AbstractRunnable implements Runnable, IRenderer
{
    private boolean      GAME_RUNNING;
    
    private boolean      isFullscreen         = false;
    private boolean      isTogglingFullScreen = false;
    
    private JFrame       frame;
    private JPanel       panel;
    
    private Cursor       defaultCursor;
    private Cursor       blankCursor;
    
    private GameListener gameListener;
    private GameConfig   config;
    private GameConsole  console;

    protected String getName()
    {
        return getClass().getSimpleName();
    }
    
    protected AbstractGame()
    {
        // panel
        panel = new JPanel(true)
        {
            private static final long serialVersionUID = -1923410016305041430L;
            
            @Override
            protected void paintComponent(Graphics g)
            {
                if (frame.isVisible())
                {
                    render(g);
                    // console
                    if (getConfig().isTrue(GameConfig.CONSOLE_ENABLED))
                    {
                        GameConsole console = getConsole();
                        if (console.isOpen())
                        {
                            console.render(g);
                        }
                    }
                }
            }
        };
        panel.setLayout(null);
        panel.setIgnoreRepaint(true);
        // frame
        frame = new JFrame(getName());
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(getDefaultFrameSize());
        frame.getContentPane().setLayout(new BorderLayout());
        if (this instanceof INotScrollable || getClass().isAnnotationPresent(NotResizable.class))
        {
            frame.getContentPane().add(panel, BorderLayout.CENTER);
        }
        else
        {
            frame.getContentPane().add(new JScrollPane(panel), BorderLayout.CENTER);
        }
        Insets insets = frame.getContentPane().getInsets();
        setPanelSize(new Dimension(getFrameSize().width  - (insets.left + insets.right),
                                   getFrameSize().height - (insets.top  + insets.bottom)));
        frame.addComponentListener(new ComponentControl());
        frame.setResizable(getClass().isAnnotationPresent(NotResizable.class) == false);
        // game listener
        gameListener = new GameListener();
        panel.addKeyListener(gameListener);
        panel.addMouseListener(gameListener);
        panel.addMouseMotionListener(gameListener);
        panel.addMouseWheelListener(gameListener);
        // listener
        gameListener.addKeyListener(new KeyAdapter()
        {
            private boolean IGNORE_KEYBOARD = false;
            
            @Override
            public void keyPressed(KeyEvent e)
            {
                if (IGNORE_KEYBOARD)
                {
                    e.consume();
                }
                else
                {
                    IGNORE_KEYBOARD = true;
                    final int stepsFPS = 5;
                    final int MAX_FPS = 1000;
                    final int MIN_FPS = 1;
                    final GameConfig config = getConfig();
                    switch (e.getKeyCode())
                    {
                        case KeyEvent.VK_MINUS:
                        {
                            if (config.isTrue(GameConfig.ALLOW_FPSCHANGE))
                            {
                                int desiredFPS = config.getInt(GameConfig.DESIRED_FPS);
                                if (desiredFPS <= 10)
                                {
                                    desiredFPS--;
                                }
                                else
                                {
                                    desiredFPS -= stepsFPS;
                                }
                                if (desiredFPS < MIN_FPS)
                                {
                                    desiredFPS = MIN_FPS;
                                }
                                if (config.getInt(GameConfig.DESIRED_FPS) != desiredFPS)
                                {
                                    config.setInt(GameConfig.DESIRED_FPS, desiredFPS);
                                    Logger.trace("-FPS: ", desiredFPS);
                                }
                            }
                            break;
                        }
                        case KeyEvent.VK_PLUS:
                        {
                            if (config.isTrue(GameConfig.ALLOW_FPSCHANGE))
                            {
                                int desiredFPS = config.getInt(GameConfig.DESIRED_FPS);
                                if (desiredFPS < 10)
                                {
                                    desiredFPS++;
                                }
                                else
                                {
                                    desiredFPS += stepsFPS;
                                }
                                if (desiredFPS > MAX_FPS)
                                {
                                    desiredFPS = MAX_FPS;
                                }
                                if (config.getInt(GameConfig.DESIRED_FPS) != desiredFPS)
                                {
                                    config.setInt(GameConfig.DESIRED_FPS, desiredFPS);
                                    Logger.trace("+FPS: ", desiredFPS);
                                }
                            }
                            break;
                        }
                        case KeyEvent.VK_F:
                        {
                            if (AbstractGame.this.getClass().isAnnotationPresent(Fullscreen.class) && e.isControlDown())
                            {
                                toggleFullscreen();
                            }
                            break;
                        }
                    }
                }
                IGNORE_KEYBOARD = false;
            }
        });
        // config
        initConfig();
        // fullscreen
        if (getClass().isAnnotationPresent(Fullscreen.class))
        {
            toggleFullscreen();
        }
        else
        {
            frame.setLocationRelativeTo(null);
            frame.setVisible(true);
        }
        // focus
        panel.requestFocus();
    }
    
    public GameListener getGameListener()
    {
        return gameListener;
    }
    
    public JFrame getFrame()
    {
        return frame;
    }
    
    protected Dimension getDefaultFrameSize()
    {
        // set frame size half the screen size
        GraphicsDevice device = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();
        Rectangle size = device.getDefaultConfiguration().getBounds();
        return new Dimension(size.width / 2, size.height / 2);
    }
    
    protected Insets getInsets()
    {
        return frame.getContentPane().getInsets();
    }

    protected GameConsole getConsole()
    {
        if (console == null)
        {
            console = new GameConsole(this);
            console.enableIngameConsole();
        }
        return console;
    }
    
    protected abstract void initConfig();
    
    protected GameConfig getConfig()
    {
        if (config == null)
        {
            config = new GameConfig();
        }
        return config;
    }
    
    protected void toggleFullscreen()
    {
        if (getClass().isAnnotationPresent(Fullscreen.class) && isTogglingFullScreen == false)
        {
            isTogglingFullScreen = true;
            GraphicsDevice device = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();
            frame.dispose();
            if (isFullscreen == false)
            {
                // make frame undecorated and not resizeable
                frame.setUndecorated(true);
                frame.setResizable(false);
                // go into full screen mode
                device.setFullScreenWindow(frame);
                isFullscreen = true;
            }
            else
            {
                // go into window mode
                frame.setVisible(false);
                device.setFullScreenWindow(null);
                frame.setUndecorated(false);
                frame.setResizable(this instanceof INotScrollable);
                isFullscreen = false;
            }
            if (this instanceof INotScrollable)
            {
                if (defaultCursor == null)
                {
                    defaultCursor = Cursor.getDefaultCursor();
                }
                if (blankCursor == null)
                {
                    // Transparent 16 x 16 pixel cursor image.
                    BufferedImage cursorImg = new BufferedImage(16, 16, BufferedImage.TYPE_INT_ARGB);
                    // Create a new blank cursor.
                    blankCursor = Toolkit.getDefaultToolkit().createCustomCursor(cursorImg, new Point(0, 0), "blank cursor");
                }
                frame.setCursor(isFullscreen ? blankCursor : defaultCursor);
            }
            frame.setVisible(true);
            // focus
            panel.requestFocus();
            isTogglingFullScreen = false;
        }
    }
    
    protected Dimension getFrameSize()
    {
        return frame.getSize();
    }
    
    protected void setFrameSize(Dimension newSize)
    {
        frame.setSize(newSize);
        frame.setLocationRelativeTo(null);
        Logger.trace("NEW-FRAME-SIZE: ", getFrameSize());
    }
    
    protected void setPanelSize(Dimension newSize)
    {
        if (panel.getPreferredSize().equals(newSize) == false)
        {
            Logger.trace("NEWSIZE: ", newSize);
            panel.setPreferredSize(newSize);
            panel.revalidate();
            if (isRunning() && this instanceof INotScrollable)
            {
                ((INotScrollable) this).recalcSize(newSize);
            }
            Logger.trace("FRAME-SIZE: ", getFrameSize());
            Logger.trace("PANEL-SIZE: ", panel.getSize());
        }
    }
    
    private class ComponentControl extends ComponentAdapter
    {
        private boolean   isResizing = false;
        private long      resizeTime = 0;
        private final int DELAY      = 600;
        
        private class ResizeThread implements Runnable
        {
            @Override
            public void run()
            {
                while (true)
                {
                    try
                    {
                        while (isResizing)
                        {
                            long delay = System.currentTimeMillis() - resizeTime;
                            if (delay > DELAY)
                            {
                                Logger.trace("RESIZING");
                                Insets insets = frame.getContentPane().getInsets();
                                Dimension size = new Dimension(newSize.width  - (insets.left + insets.right),
                                                               newSize.height - (insets.top  + insets.bottom));
                                if (frame.getExtendedState() != JFrame.MAXIMIZED_BOTH)
                                {
                                    setFrameSize(size);
                                    if (AbstractGame.this instanceof INotScrollable)
                                    {
                                        setPanelSize(size);
                                    }
                                }
                                isResizing = false;
                            }
                            else
                            {
                                Thread.sleep(delay);
                            }
                        }
                        Thread.sleep(DELAY);
                    }
                    catch (InterruptedException e)
                    {
                        // e.printStackTrace(System.err);
                    }
                }
            }
        }
        
        private Dimension newSize;
        private Thread    resizeThread;
        
        public ComponentControl()
        {
            resizeThread = new Thread(new ResizeThread(), "Resizer");
            resizeThread.start();
        }
        
        @Override
        public void componentResized(ComponentEvent e)
        {
            if (isFullscreen == false)
            {
                if (e.getComponent().getSize().equals(newSize) == false)
                {
                    Logger.trace("INIT-RESIZING");
                    newSize = e.getComponent().getSize();
                    resizeTime = System.currentTimeMillis();
                    if (isResizing == false)
                    {
                        isResizing = true;
                        resizeThread.interrupt();
                    }
                }
            }
        }
    }
    
    @Override
    public void update()
    {
        super.update();
        // title
        StringBuilder title = new StringBuilder(getName()).append(" - ");
        title.append(getConfig().getInt(GameConfig.DESIRED_FPS)).append(" FPS; ");
        title.append(getStatus()).append("; ");
        title.append(getInfo());
        frame.setTitle(title.toString());
    }
    
    @Override
    public void run()
    {
        reset();
        start();
        // game loop
        GAME_RUNNING = true;
        long lastRepaint = -1;
        long lastUpdate  = -1;
        while (GAME_RUNNING)
        {
            try
            {
                final long frameTime = 1000 / getConfig().getInt(GameConfig.DESIRED_FPS);
                final long startTime = System.currentTimeMillis();
                long currentTime = startTime;
                // detect time
                long diffTime = currentTime - lastRepaint;
                // check if desired fps is reached
                if (diffTime > frameTime)
                {
                    SwingUtilities.invokeAndWait(new Runnable() {
                        
                        @Override
                        public void run()
                        {
                            panel.repaint();
                        }
                    });
                    lastRepaint = System.currentTimeMillis();
                }
                // update
                currentTime = System.currentTimeMillis();
                diffTime = currentTime - lastUpdate;
                if (diffTime > frameTime)
                {
                    update();
                    lastUpdate = System.currentTimeMillis();
                }
                // detect runtime
                long runTime = System.currentTimeMillis() - startTime;
                // magic to detect how long we would run with desired fps
                long runTimePerSec = runTime * getConfig().getInt(GameConfig.DESIRED_FPS);
                // is it longer then 1 sec (1000 ms)?
                long waitTimePerSec = 1000 - runTimePerSec;
                if (waitTimePerSec > 0)
                {
                    long waitTime = waitTimePerSec / getConfig().getInt(GameConfig.DESIRED_FPS);
                    if (waitTime > 0)
                    {
                        Thread.yield();
                        Thread.sleep(waitTime);
                    }
                }
            }
            catch (Exception ex)
            {
                GAME_RUNNING = false;
                Logger.showExceptionError(ex);
            }
        }
    }
}
